Python environment setup
2026-01-01 → 2026-01-03
Modern Python environment setup using uv.
The problem#
Python’s dependency management has historically been messy. Projects need:
- Isolation — different projects need different package versions
- Reproducibility — collaborators must get the exact same environment
- Python version control — projects may require specific Python versions
Traditional tools (pip, venv, requirements.txt) don’t solve all of these well.
Modern solution: uv#
uv is a fast Python package manager (written in Rust) that handles virtual environments, dependency resolution, and lockfiles in one tool.
Quick start#
uv init # Initialize project (creates pyproject.toml)
uv add pandas numpy # Add dependencies
uv sync # Install everything from lockfile
That’s it. uv automatically:
- Creates
.venv/if it doesn’t exist - Generates
uv.lockwith exact versions of all dependencies (including transitive ones) - Installs everything reproducibly
Avoid mixing with pip or using uv pip — stick to uv add/uv sync to keep pyproject.toml and uv.lock in sync.
Common commands#
| Command | Description |
|---|---|
uv init |
Initialize new project |
uv add PKG |
Add dependency |
uv add --dev PKG |
Add dev dependency |
uv remove PKG |
Remove dependency |
uv sync |
Install from lockfile |
uv lock |
Update lockfile without installing |
uv run CMD |
Run command in the environment |
uvx TOOL |
Run one-off tool without installing |
Project structure#
myproject/
├── .python-version # Python version for this project
├── .venv/ # Virtual environment (gitignored)
├── pyproject.toml # Project metadata + dependencies
└── uv.lock # Lockfile (commit this!)
Key files#
.python-version#
A simple file containing the Python version:
3.12
uv reads this and uses the correct Python version. This file is understood by many tools (pyenv, uv, etc.).
pyproject.toml#
The modern standard for Python project configuration (PEP 517, PEP 518). It replaces setup.py, setup.cfg, and requirements.txt.
[project]
name = "myproject"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"pandas>=2.0",
"numpy>=1.26",
]
[project.optional-dependencies]
dev = ["pytest", "ruff"]
Why pyproject.toml is better than requirements.txt:
- Standardized — one format for all Python tools
- Structured — metadata, dependencies, and tool configs in one place
- Semantic versioning — specify version ranges, not just exact versions
- Optional dependencies — group dev/test/docs dependencies separately
uv.lock#
Auto-generated lockfile with exact versions of all dependencies (including transitive). You want to commit this file — it ensures reproducibility across machines.
Auto-activation with direnv#
direnv automatically activates environments when you enter a project directory.
- Add to
~/.direnvrc:
use_uv() {
[ -d .venv ] || uv venv
source .venv/bin/activate
[ -f uv.lock ] && uv sync
}
- Create
.envrcin your project:
echo "use uv" > .envrc
- Allow it:
direnv allow
Now when you cd into the project, the environment activates automatically.
Linting and formatting with ruff#
ruff is an extremely fast Python linter and formatter (also written in Rust). It replaces flake8, isort, black, and many other tools.
Setup#
uv add --dev ruff
Add configuration to pyproject.toml:
[tool.ruff]
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I"] # E=pycodestyle, F=pyflakes, I=isort
Running manually#
ruff check . # Lint
ruff check --fix . # Lint and auto-fix
ruff format . # Format
Format on save (editor integration)#
My favorite way to use ruff is setting up “format on save” for my editors so that I don’t even need to think about formatting at all. Once you set up, every file save fixes all the formatting issues automatically.
VS Code: Install the Ruff extension and enable format on save. See VS Code setup docs.
Neovim: Install ruff via Mason, then configure with nvim-lspconfig. See Neovim setup docs.